/*
 * Copyright 2013-2018 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.ww.microrpc.sentinel;

import com.ww.microrpc.rpc.*;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.util.Map;

/**
 * {@link MicroRpc.Builder} like {@link HystrixMicroRpc.Builder}.
 *
 * @author <a href="mailto:fangjian0423@gmail.com">Jim</a>
 * @author ww
 */
public final class SentinelMicroRpc {

    private SentinelMicroRpc() {

    }

    public static Builder builder() {
        return new Builder();
    }

    public static final class Builder extends MicroRpc.Builder
            implements ApplicationContextAware {

        private Contract contract = new Contract.Default();

        private ApplicationContext applicationContext;

//		private MicroRpcContext MicroRpcContext;

        @Override
        public MicroRpc.Builder invocationHandlerFactory(
                InvocationHandlerFactory invocationHandlerFactory) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Builder contract(Contract contract) {
            this.contract = contract;
            return this;
        }

        @Override
        public MicroRpc build() {
            super.invocationHandlerFactory(new InvocationHandlerFactory() {
                @Override
                public InvocationHandler create(Target target,
                                                Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
                    // using reflect get fallback and fallbackFactory properties from
                    // MicroRpcClientFactoryBean because MicroRpcClientFactoryBean is a package
                    // level class, we can not use it in our package
                    Object microRpcClientFactoryBean = Builder.this.applicationContext
                            .getBean("&" + target.type().getName());

                    Class fallback = (Class) getFieldValue(microRpcClientFactoryBean,
                            "fallback");
                    Class fallbackFactory = (Class) getFieldValue(microRpcClientFactoryBean,
                            "fallbackFactory");
                    String beanName = (String) getFieldValue(microRpcClientFactoryBean,
                            "contextId");
                    if (!StringUtils.hasText(beanName)) {
                        beanName = (String) getFieldValue(microRpcClientFactoryBean, "name");
                    }

                    Object fallbackInstance;
                    FallbackFactory fallbackFactoryInstance;
                    // check fallback and fallbackFactory properties
                    if (void.class != fallback) {
                        fallbackInstance = getFromContext(beanName, "fallback", fallback,
                                target.type());
                        return new SentinelInvocationHandler(target, dispatch,
                                new FallbackFactory.Default(fallbackInstance));
                    }
                    if (void.class != fallbackFactory) {
                        fallbackFactoryInstance = (FallbackFactory) getFromContext(
                                beanName, "fallbackFactory", fallbackFactory,
                                FallbackFactory.class);
                        return new SentinelInvocationHandler(target, dispatch,
                                fallbackFactoryInstance);
                    }
                    return new SentinelInvocationHandler(target, dispatch);
                }

                private Object getFromContext(String name, String type,
                                              Class fallbackType, Class targetType) {
                    Object fallbackInstance =
//                            microRpcContext.getInstance(name, fallbackType);
                            applicationContext.getBean(fallbackType);
                    if (fallbackInstance == null) {
                        throw new IllegalStateException(String.format(
                                "No %s instance of type %s found for microRpc client %s",
                                type, fallbackType, name));
                    }

                    if (!targetType.isAssignableFrom(fallbackType)) {
                        throw new IllegalStateException(String.format(
                                "Incompatible %s instance. Fallback/fallbackFactory of type %s is not assignable to %s for microRpc client %s",
                                type, fallbackType, targetType, name));
                    }
                    return fallbackInstance;
                }
            });

            super.contract(new SentinelContractHolder(contract));
            return super.build();
        }

        private Object getFieldValue(Object instance, String fieldName) {
            Field field = ReflectionUtils.findField(instance.getClass(), fieldName);
            field.setAccessible(true);
            try {
                return field.get(instance);
            } catch (IllegalAccessException e) {
                // ignore
            }
            return null;
        }

        @Override
        public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
            this.applicationContext = applicationContext;
        }

//		@Override
//		public void setApplicationContext(ApplicationContext applicationContext)
//				throws BeansException {
//			this.applicationContext = applicationContext;
//			microRpcContext = this.applicationContext.getBean(MicroRpcContext.class);
//		}

    }

}
